=begin License
@package    RubyBitTorrent
@version    $Revision: 5 $ $Date: 2007-01-31 15:13:51 +0000 (Wed, 31 Jan 2007) $ ($Author: infinity.of.zero $)
@author     Ben de Graaff
@copyright  (c) 2007 Ben de Graaff

Copyright (c) 2007, B. de Graaff
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions
  and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions
  and the following disclaimer in the documentation and/or other materials provided with the
  distribution.
* Neither the name of the product nor the names of its contributors may be used to endorse or
  promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=end

module BitTorrent
    require 'dictionary'


    class NotValidTorrentMeta < StandardError; end
    class OutOfBoundsError < NotValidTorrentMeta; end
    class UnknownTypeError < StandardError; end


    class MetaDecode
        attr :buffer
        attr :size
        attr :offset


        # Static method to decode a file
        def self.decode_file( file )
            fct = self.new

            File.open( file, 'rb' ) do |fp|
                fct.decode_buffer fp.read
            end
        end


        # Static method to decode a string
        def self.decode( buffer )
            fct = self.new
            fct.decode_buffer buffer
        end


        # Method to automagically handle the decoding
        def decode_buffer( buffer )
            @buffer = buffer
            @size   = @buffer.length
            @offset = 0
            r = decode_func
            @buffer = nil
            @offset = 0
            @size   = 0
            return r
        end


        protected


        # Internal parsing function. Parses the serialized data and
        #  converts it back to the correct type.
        def decode_func
            raise OutOfBoundsError.new  if @offset >= @size

            # Figure out what kind of variable we're dealing with
            case @buffer[ @offset ]
                when 100;    return decode_dict   # d
                when 108;    return decode_list   # l
                when 105;    return decode_int    # i
                when 48..57; return decode_string # 0-9
            end

            raise NotValidTorrentMeta.new( 'Invalid datatype' )
        end


        # Integer decoding (format: i<num..>e)
        def decode_int
            @offset += 1

            c = @buffer.index( 101, @offset ) # e
            raise NotValidTorrentMeta.new( 'Could not find end of integer' )  if c === false

            n = @buffer[ @offset, c - @offset ]
            raise NotValidTorrentMeta.new( 'Expected numeric value' )  if /^[0-9]+$/ !~ n

            @offset = c + 1
            return n.to_i
        end


        # String decoding (format: <length>:<str..>)
        def decode_string
            c = @buffer.index( 58, @offset ) # :
            raise NotValidTorrentMeta.new( 'Could not find end of string length' )  if c === false

            n = @buffer[ @offset, c - @offset ]
            raise NotValidTorrentMeta.new( 'Expected numeric string length' )  if /^[0-9]+$/ !~ n

            n = n.to_i

            raise OutOfBoundsError.new  if c + n >= @size

            str    = @buffer[ c + 1, n ]
            @offset = c + 1 + n
            return str
        end


        # List decoding (format: l<value><value><..>e)
        def decode_list
            raise OutOfBoundsError.new  if @offset + 1 >= @size

            @offset += 1
            list = Array.new

            while @buffer[ @offset ] != 101 # e
                list.push( decode_func )
            end

            @offset += 1
            return list
        end


        # Dictionary decoding (format: d<key><value><..>e)
        def decode_dict
            raise OutOfBoundsError.new  if @offset + 1 >= @size

            @offset += 1
            #dict = Hash.new
            dict = Dictionary.new

            while @buffer[ @offset ] != 101 # e
                # Key - Should always be a string..
                nk = decode_func

                raise NotValidTorrentMeta.new( 'Expected String class' )  if nk.class != String

                # Value
                nv = decode_func
                raise NotValidTorrentMeta.new( 'No value for dictionary entry' )  if nv === false

                dict[ nk ] = nv
            end

            @offset += 1
            return dict
        end
    end


    class MetaEncode
        def self.encode_file( file, obj )
            fct    = self.new
            buffer = fct.encode_object( obj )
            fct    = nil

            File.open( file, 'wb' ) do |fp|
                fp.write buffer
            end
        end


        def self.encode( obj )
            fct = self.new
            fct.encode_object( obj )
        end


        def encode_object( obj )
            encode_func( obj )
        end


        protected

        def encode_func( obj )
            if obj.kind_of?( String )
                return obj.length.to_s + ':' + obj.to_s
            elsif obj.kind_of?( Array )
                r = 'l'
                obj.each { |v| r += encode_func( v ); }
                return r += 'e'
            elsif obj.kind_of?( Dictionary )
                    r = 'd'
                    obj.each { |k,v| r << encode( k.to_s ) << encode_func( v ) }
                    return r << 'e'
            elsif obj.kind_of?( Integer )
                return 'i' + obj.to_s + 'e'
            end

            raise UnknownTypeError.new( 'Unknown class: "' + obj.class.to_s + '"' )
        end
    end
end

=begin testing
p BitTorrent::MetaDecode.decode_file( 'test.torrent' )
p BitTorrent::MetaDecode.decode( 'i666e' )
d = BitTorrent::MetaDecode.new
p d.decode_buffer( '1:.' )
p d.decode_buffer( '2:!!' )
p d.decode_buffer( '3:???' )
p BitTorrent::MetaDecode.new.decode_buffer( '4:%%%%' )
p BitTorrent::MetaEncode.encode_file( 'ruby.torrent', [1,2,3] )
p BitTorrent::MetaDecode.decode_file( 'ruby.torrent' )
=end